Conversation
d95f090 to
f56b57d
Compare
f56b57d to
2afa209
Compare
…id deps to be properly resolved by Gradle when consumed
…checked cast compilation warnings
| ``` | ||
|
|
||
| This will take care of linking the expo dependencies like `expo-image` to your AAR. | ||
| The plugin supports Expo projects out of the box. Publishing the AAR to Maven Local will also publish the Expo dependencies to Maven Local so that they can be resolved when building the brownfield app. |
There was a problem hiding this comment.
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces first-class Expo support for the React Native Brownfield toolchain by adding an Expo config plugin (to automate native setup during expo prebuild), extending the CLI to detect Expo projects and run prebuild/codegen as needed, updating the Gradle plugin for Expo dependency handling, and adding an Expo demo app plus updated CI road tests.
Changes:
- Added an Expo config plugin in
@callstack/react-native-brownfieldthat generates/patches native iOS/Android project structures for XCFramework/AAR packaging. - Updated the Brownfield CLI to detect Expo projects, run
expo prebuildwhen applicable, and improved Brownie codegen behavior when no stores exist. - Added an
apps/ExpoAppdemo and updatedAndroidApp/AppleApp+ CI workflows to test Expo and vanilla variants in parallel.
Reviewed changes
Copilot reviewed 150 out of 164 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Removes workspace TS project references at repo root. |
| packages/react-native-brownfield/tsconfig.json | Expands TS include set for library sources/package.json. |
| packages/react-native-brownfield/src/expo-config-plugin/withBrownfield.ts | New Expo config plugin entry with config resolution and platform plugin composition. |
| packages/react-native-brownfield/src/expo-config-plugin/logging.ts | Adds plugin-scoped logger with debug gating. |
| packages/react-native-brownfield/src/expo-config-plugin/index.ts | Exposes Expo config plugin default export. |
| packages/react-native-brownfield/src/expo-config-plugin/app.plugin.ts | Adds Expo config plugin entrypoint for app.plugin resolution. |
| packages/react-native-brownfield/src/expo-config-plugin/errors/SourceModificationError.ts | Adds plugin-specific error type for source modifications. |
| packages/react-native-brownfield/src/expo-config-plugin/types/index.ts | Adds type re-exports for plugin configuration surface. |
| packages/react-native-brownfield/src/expo-config-plugin/types/BrownfieldPluginConfig.ts | Defines user config + resolved config types. |
| packages/react-native-brownfield/src/expo-config-plugin/types/RenderedTemplateFile.ts | Defines rendered template file primitive. |
| packages/react-native-brownfield/src/expo-config-plugin/types/ios/BrownfieldPluginIosConfig.ts | Adds iOS plugin config shape and resolved type. |
| packages/react-native-brownfield/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts | Adds Android plugin config shape and resolved type. |
| packages/react-native-brownfield/src/expo-config-plugin/template/engine.ts | Adds template renderer for plugin-generated native files. |
| packages/react-native-brownfield/src/expo-config-plugin/template/ios/FrameworkInterface.swift | Template for framework interface exports and bundle lookup. |
| packages/react-native-brownfield/src/expo-config-plugin/template/ios/Info.plist | Template Info.plist for generated framework target. |
| packages/react-native-brownfield/src/expo-config-plugin/template/ios/PodfileTargetBlock.rb | Template Podfile target block for framework target. |
| packages/react-native-brownfield/src/expo-config-plugin/template/ios/ReactNativeHostManager.swift | Template host manager for Expo iOS integration. |
| packages/react-native-brownfield/src/expo-config-plugin/template/ios/patchExpoPre55.sh | Template patch script for Expo SDK <55 ExpoModulesProvider.swift. |
| packages/react-native-brownfield/src/expo-config-plugin/template/android/build.gradle.kts | Template Gradle build for generated AAR module. |
| packages/react-native-brownfield/src/expo-config-plugin/template/android/gradle.properties | Template Gradle properties for generated module. |
| packages/react-native-brownfield/src/expo-config-plugin/template/android/proguard-rules.pro | Template proguard rules placeholder. |
| packages/react-native-brownfield/src/expo-config-plugin/template/android/AndroidManifest.xml | Template AndroidManifest for generated module. |
| packages/react-native-brownfield/src/expo-config-plugin/template/android/ReactNativeHostManager.kt | Template host manager for Expo Android integration. |
| packages/react-native-brownfield/src/expo-config-plugin/template/android/consumer-rules.pro | Template consumer rules placeholder. |
| packages/react-native-brownfield/src/expo-config-plugin/ios/withBrownfieldIos.ts | Implements iOS plugin: Xcode target, Podfile edits, file generation. |
| packages/react-native-brownfield/src/expo-config-plugin/ios/xcodeHelpers.ts | Adds helpers for Xcode target/build phases/build settings. |
| packages/react-native-brownfield/src/expo-config-plugin/ios/podfileHelpers.ts | Adds Podfile modification + (pre-55) phase reordering hook injection. |
| packages/react-native-brownfield/src/expo-config-plugin/ios/withIosFrameworkFiles.ts | Generates iOS framework directory and template files. |
| packages/react-native-brownfield/src/expo-config-plugin/ios/index.ts | Exports iOS plugin helpers. |
| packages/react-native-brownfield/src/expo-config-plugin/android/withBrownfieldAndroid.ts | Implements Android plugin: settings/build.gradle changes + file generation. |
| packages/react-native-brownfield/src/expo-config-plugin/android/withAndroidModuleFiles.ts | Generates Android module files from templates and resolved config. |
| packages/react-native-brownfield/src/expo-config-plugin/android/gradleHelpers.ts | Adds helpers to mutate root build.gradle and settings.gradle. |
| packages/react-native-brownfield/src/expo-config-plugin/android/constants.ts | Defines brownfield Gradle plugin coordinate/version constant. |
| packages/react-native-brownfield/src/expo-config-plugin/android/index.ts | Exports Android plugin helpers. |
| packages/react-native-brownfield/package.json | Bumps version and adds export for ./app.plugin.js. |
| packages/cli/src/brownie/store-discovery.ts | Throws typed NoBrownieStoresError when no stores found. |
| packages/cli/src/brownie/errors/NoBrownieStoresError.ts | Adds typed error for “no stores” case. |
| packages/cli/src/brownie/helpers/runBrownieCodegenIfApplicable.ts | Adds helper to run Brownie codegen only if Brownie is installed. |
| packages/cli/src/brownie/commands/codegen.ts | Handles “no stores” gracefully (log + return instead of hard failure). |
| packages/cli/src/brownfield/utils/rn-cli.ts | Removes old RN CLI project discovery helper file. |
| packages/cli/src/brownfield/utils/project.ts | Adds unified project discovery with Expo augmentation + Expo detection. |
| packages/cli/src/brownfield/utils/paths.ts | Switches to in-place mutation (drops cloneDeep) for relative sourceDir. |
| packages/cli/src/brownfield/utils/index.ts | Removes barrel export for utilities. |
| packages/cli/src/brownfield/utils/expo.ts | Adds expo prebuild runner utility for CLI flows. |
| packages/cli/src/brownfield/index.ts | Removes re-export of removed utils barrel. |
| packages/cli/src/brownfield/commands/publishAndroid.ts | Runs Expo prebuild + Brownie codegen before publish flow. |
| packages/cli/src/brownfield/commands/packageAndroid.ts | Runs Expo prebuild + Brownie codegen before AAR packaging. |
| packages/cli/src/brownfield/commands/packageIos.ts | Runs Expo prebuild + Brownie codegen; adjusts .brownfield paths. |
| packages/cli/package.json | Bumps version and adds @expo/config; bumps rock-js deps; removes lodash.clonedeep. |
| packages/brownie/tsconfig.json | Minor formatting change (trailing comma removal). |
| packages/brownie/package.json | Removes react devDependency (aligns with workspace strategy). |
| package.json | Adds @expo/config-plugins / @expo/config-types; adds signed/unsigned publish scripts. |
| gradle-plugins/react/gradle/libs.versions.toml | Adds versioncompare lib; formatting normalization. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/StringMatcher.kt | Adds string matching abstraction (literal/regex) for artifact filtering. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/StringExtensions.kt | Adds String.capitalized() helper. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt | Removes isExpo flag from Gradle plugin extension. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Constants.kt | Adds Expo transitive dependency filtering constants + matchers. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantTaskProvider.kt | Uses capitalized() helper; minor formatting improvements. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantProcessor.kt | Uses capitalized() helper; formatting improvements. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantPackagesProperty.kt | Adds suppression for unchecked cast and formatting. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantHelper.kt | Uses capitalized() helper; formatting; minor type simplification. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ProguardProcessor.kt | Uses capitalized() helper; improves output-file coercion formatting. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/JNILibsProcessor.kt | Uses capitalized() helper; formatting improvements. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt | Adjusts source set wiring; moves autolinking java dir to main; formatting. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt | Switches to Expo auto-detection + ExpoPublishingHelper integration. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/ProjectConfigurations.kt | Uses capitalized() helper for variant naming. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/VersionMediatingDependencySet.kt | Adds version-mediating set for Expo transitive dependency resolution. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/ReflectionUtils.kt | Adds reflection proxy helpers for Expo Gradle plugin interoperability. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/ExpoProjectionPrimitives.kt | Adds projection interfaces for reflected Expo Gradle types. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/BrownfieldPrimitives.kt | Adds publishing/dependency primitives for Expo publishing logic. |
| gradle-plugins/react/brownfield/src/main/kotlin/com/callstack/react/brownfield/artifacts/ArtifactsResolver.kt | Plumbs “hasExpo” flag; minor refactors/formatting. |
| gradle-plugins/react/brownfield/build.gradle.kts | Adds snapshot versioning and skip-signing plumbing; adds versioncompare dep. |
| gradle-plugins/react/README.md | Updates Expo support docs (now automatic). |
| gradle-plugins/publish-to-maven-local.sh | Adds --skip-signing flow and snapshot property wiring. |
| docs/docs/docs/guides/guidelines.mdx | Normalizes JS string quotes in example. |
| docs/docs/docs/getting-started/quick-start.mdx | Links “built-in CLI” reference to CLI docs. |
| docs/docs/docs/getting-started/introduction.mdx | Mentions Expo config plugin in feature list. |
| docs/docs/docs/getting-started/expo.mdx | Adds Expo integration guide and plugin options documentation. |
| docs/docs/docs/getting-started/examples.mdx | Adds ExpoApp and explains flavors/configurations for demo apps. |
| docs/docs/docs/getting-started/_meta.json | Adds Expo page to docs navigation. |
| apps/TesterIntegrated/package.json | Switches to workspace protocol for internal dependencies. |
| apps/TesterIntegrated/BrownfieldStore.brownie.ts | Exports store interfaces (for codegen/module augmentation). |
| apps/TesterIntegrated/App.tsx | Reorders Brownie store import to run earlier. |
| apps/RNApp/src/utils.ts | Extracts random theme utils. |
| apps/RNApp/src/navigation/RootStack.ts | Adds navigation type + stack creation in separate module. |
| apps/RNApp/src/components/counter/index.tsx | Adds placeholder counter component for non-iOS. |
| apps/RNApp/src/components/counter/index.ios.tsx | Adds iOS-specific counter implementation using Brownie store. |
| apps/RNApp/src/HomeScreen.tsx | Splits Home screen into its own module; uses utils + counter. |
| apps/RNApp/src/App.tsx | Adds App wrapper around navigation + store import. |
| apps/RNApp/package.json | Switches to workspace protocol deps; removes unused new-app-screen dep. |
| apps/RNApp/ios/Podfile.lock | Updates pod versions/checksums (Brownie/ReactBrownfield/Yoga). |
| apps/RNApp/index.js | Updates App import path to new src/App. |
| apps/RNApp/android/build.gradle | Uses snapshot brownfield gradle plugin version. |
| apps/RNApp/android/BrownfieldLib/build.gradle.kts | Switches publishing version to snapshot; fixes doc typo in comments. |
| apps/RNApp/BrownfieldStore.brownie.ts | Exports store interfaces (for codegen/module augmentation). |
| apps/RNApp/App.tsx | Removes old monolithic App implementation (moved to src/). |
| apps/README.md | Documents ExpoApp + AndroidApp flavors and AppleApp variants. |
| apps/ExpoApp/tsconfig.json | Adds Expo app TS config. |
| apps/ExpoApp/scripts/prepare-android-build-gradle-for-ci.ts | Patches Expo Android build.gradle to use mavenLocal + snapshot plugin in CI. |
| apps/ExpoApp/package.json | Adds Expo app dependencies and brownfield scripts. |
| apps/ExpoApp/hooks/use-theme-color.ts | Adds theme color hook. |
| apps/ExpoApp/hooks/use-color-scheme.web.ts | Adds hydration-safe web color scheme hook. |
| apps/ExpoApp/hooks/use-color-scheme.ts | Re-exports RN hook for native. |
| apps/ExpoApp/eslint.config.mjs | Adds Expo app ESLint config overrides. |
| apps/ExpoApp/entry.tsx | Adds dual registration: RNApp component and Expo default main. |
| apps/ExpoApp/constants/theme.ts | Adds theme constants/fonts. |
| apps/ExpoApp/components/ui/icon-symbol.tsx | Adds icon mapping for non-iOS platforms. |
| apps/ExpoApp/components/ui/icon-symbol.ios.tsx | Adds iOS symbol icon component. |
| apps/ExpoApp/components/ui/collapsible.tsx | Adds collapsible UI component. |
| apps/ExpoApp/components/themed-view.tsx | Adds themed view component. |
| apps/ExpoApp/components/themed-text.tsx | Adds themed text component. |
| apps/ExpoApp/components/parallax-scroll-view.tsx | Adds parallax scroll view component. |
| apps/ExpoApp/components/hello-wave.tsx | Adds animated hello wave component. |
| apps/ExpoApp/components/haptic-tab.tsx | Adds haptic tab button component. |
| apps/ExpoApp/components/external-link.tsx | Adds external link component with in-app browser behavior. |
| apps/ExpoApp/components/counter/index.tsx | Adds placeholder counter component. |
| apps/ExpoApp/components/counter/index.ios.tsx | Adds iOS-specific counter using Brownie store. |
| apps/ExpoApp/assets/images/react-logo.png | Adds image asset. |
| apps/ExpoApp/assets/images/partial-react-logo.png | Adds image asset. |
| apps/ExpoApp/assets/images/favicon.png | Adds image asset. |
| apps/ExpoApp/assets/images/android-icon-monochrome.png | Adds image asset. |
| apps/ExpoApp/app/modal.tsx | Adds modal route screen. |
| apps/ExpoApp/app/_layout.tsx | Adds app root layout with stack and theme provider. |
| apps/ExpoApp/app/(tabs)/index.tsx | Adds home tab route. |
| apps/ExpoApp/app/(tabs)/explore.tsx | Adds explore tab route. |
| apps/ExpoApp/app/(tabs)/_layout.tsx | Adds tabs layout. |
| apps/ExpoApp/app.json | Adds Expo config including brownfield plugin usage. |
| apps/ExpoApp/RNApp.tsx | Adds simple RN surface used for native embedding. |
| apps/ExpoApp/README.md | Adds default Expo README for the demo app. |
| apps/ExpoApp/BrownfieldStore.brownie.ts | Adds Brownie store module augmentation for Expo app. |
| apps/ExpoApp/.gitignore | Adds ignore rules including generated native folders. |
| apps/AppleApp/prepareXCFrameworks.js | Adds script to copy/rename built XCFrameworks into AppleApp package dir. |
| apps/AppleApp/package.json | Adds scripts for expo/vanilla variants; switches to ESM and adds dev deps. |
| apps/AppleApp/Brownfield-Apple-App-Info.plist | Adds minimal Info.plist file. |
| apps/AppleApp/.gitignore | Ignores generated package/ directory. |
| apps/AndroidApp/package.json | Splits build scripts by flavor (expo/vanilla). |
| apps/AndroidApp/gradle/libs.versions.toml | Adds separate deps for Expo vs vanilla AAR coordinates; adds Material. |
| apps/AndroidApp/gradle.properties | Minor whitespace tweak. |
| apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/ReactNativeHostManager.kt | Adds flavor-specific alias + no-op config-change handler for parity. |
| apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt | Adds shared constant for module name in vanilla flavor. |
| apps/AndroidApp/app/src/main/res/values/themes.xml | Fixes typo in theme name. |
| apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt | Wires config changes and uses constant module name. |
| apps/AndroidApp/app/src/main/AndroidManifest.xml | Fixes theme reference name. |
| apps/AndroidApp/app/src/expo/java/com/callstack/brownfield/android/example/ReactNativeHostManager.kt | Adds flavor-specific alias to Expo-generated host manager. |
| apps/AndroidApp/app/src/expo/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt | Adds shared constant for module name in Expo flavor. |
| apps/AndroidApp/app/build.gradle.kts | Adds product flavors and flavor-specific brownfieldlib dependencies. |
| CONTRIBUTING.md | Updates scripts list and adds new consumer build variants. |
| .github/workflows/ci.yml | Splits Android/iOS road tests into expo and vanilla variants using composite actions. |
| .github/actions/appleapp-road-test/action.yml | Adds reusable composite action for AppleApp road tests. |
| .github/actions/androidapp-road-test/action.yml | Adds reusable composite action for AndroidApp road tests. |
| .changeset/config.json | Adds Expo example app package to ignored list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ) -> UIView { | ||
| let bundleURL = reactNativeDelegate?.bundleURL() | ||
| return (expoDelegate?.recreateRootView( | ||
| withBundleURL: bundleURL, moduleName: moduleName, initialProps: initialProps, | ||
| launchOptions: launchOptions))! | ||
| } |
There was a problem hiding this comment.
loadView force-unwraps the result of expoDelegate?.recreateRootView(...), which will crash if initialize() wasn't called or if Expo returns nil. Consider guarding and throwing/fatalError with a clear message (or returning an optional) to avoid a hard-to-debug runtime crash.
| # Path to ExpoModulesProvider.swift | ||
| FILE="${SRCROOT}/Pods/Target Support Files/Pods-ExpoApp-{{FRAMEWORK_NAME}}/ExpoModulesProvider.swift" | ||
|
|
There was a problem hiding this comment.
The patch script hardcodes Pods-ExpoApp-{{FRAMEWORK_NAME}} in the path. For consumers whose app target isn't named ExpoApp, the file won't be found and the patch won't run. Consider parameterizing the app target name (or locating ExpoModulesProvider.swift dynamically) so the plugin works in arbitrary projects.
| export default createRunOncePlugin( | ||
| withBrownfield, | ||
| process.env.npm_package_name as string, | ||
| process.env.npm_package_version as string | ||
| ); |
There was a problem hiding this comment.
createRunOncePlugin is passed process.env.npm_package_name / npm_package_version. Those env vars are not guaranteed to be set in all execution contexts, which can cause the plugin name/version to become undefined and break run-once semantics. Prefer a stable literal (package name) and a version sourced from package.json at build time.
| ios: expoConfig.ios | ||
| ? { | ||
| frameworkName: config.ios?.frameworkName ?? 'BrownfieldLib', | ||
| bundleIdentifier: | ||
| config.ios?.bundleIdentifier ?? | ||
| `${expoConfig.ios.bundleIdentifier}.brownfield`, | ||
| buildSettings: config.ios?.buildSettings ?? {}, | ||
| deploymentTarget: config.ios?.deploymentTarget ?? '15.0', | ||
| frameworkVersion: config.ios?.frameworkVersion ?? '1', | ||
| } | ||
| : null, |
There was a problem hiding this comment.
bundleIdentifier default uses ${expoConfig.ios.bundleIdentifier}.brownfield, but expoConfig.ios.bundleIdentifier can be undefined. This would silently produce undefined.brownfield. Consider validating and throwing a clear error when the bundle identifier (or android package) is missing.
| // arg parser | ||
| import yargs from 'yargs'; | ||
|
|
||
| const { appName } = yargs(process.argv.slice(2)) | ||
| .usage('prepareXCFrameworks --appName <appName>') | ||
| .demandOption('appName', 'App name is required, pass it as an argument') | ||
| .parse(); | ||
|
|
There was a problem hiding this comment.
This file uses yargs, but apps/AppleApp/package.json doesn't declare it. In a workspace install this may not be available and the build script will fail at runtime. Add yargs as a (dev)dependency for AppleApp, or replace it with a minimal process.argv parser.
| // arg parser | |
| import yargs from 'yargs'; | |
| const { appName } = yargs(process.argv.slice(2)) | |
| .usage('prepareXCFrameworks --appName <appName>') | |
| .demandOption('appName', 'App name is required, pass it as an argument') | |
| .parse(); | |
| // minimal arg parser to avoid external dependency on "yargs" | |
| function parseArgs(argv) { | |
| let appName; | |
| for (let i = 0; i < argv.length; i++) { | |
| const arg = argv[i]; | |
| if (arg === '--appName') { | |
| appName = argv[i + 1]; | |
| break; | |
| } | |
| if (arg.startsWith('--appName=')) { | |
| appName = arg.split('=', 2)[1]; | |
| break; | |
| } | |
| } | |
| return { appName }; | |
| } | |
| const { appName } = parseArgs(process.argv.slice(2)); |
| export function isExpoProject(projectRoot: string): boolean { | ||
| const hasExpoConfig = getExpoConfigIfIsExpo(projectRoot) !== null; | ||
|
|
||
| // additionally, it is needed to check if the project depends on Expo packages explicitly | ||
| // to prevent false positives in a monorepo setup | ||
| const rnProjectRoot = findProjectRoot(); | ||
| const packageJsonPath = path.join(rnProjectRoot, 'package.json'); | ||
| const dependsOnExpo = | ||
| fs.existsSync(packageJsonPath) && | ||
| ['dependencies', 'peerDependencies', 'devDependencies'].some( | ||
| (key) => JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))[key]?.expo | ||
| ); | ||
|
|
||
| return hasExpoConfig && dependsOnExpo; |
There was a problem hiding this comment.
isExpoProject(projectRoot) calls findProjectRoot() again to locate package.json, ignoring the projectRoot parameter. This can mis-detect Expo when the CLI is invoked from a different working directory or when projectRoot is explicitly provided. Use the passed projectRoot consistently (and parse package.json once instead of re-reading it per dependency key).
| const templatePath = path.join(__dirname, platform, name); | ||
|
|
||
| let templateContent = fs.readFileSync(templatePath, 'utf8'); | ||
|
|
||
| for (const [key, value] of Object.entries(params ?? {})) { | ||
| const regex = new RegExp(key, 'g'); | ||
| templateContent = templateContent.replace(regex, String(value)); | ||
| } |
There was a problem hiding this comment.
renderTemplate builds a RegExp directly from the template key (e.g. {{FRAMEWORK_NAME}}). Curly braces are special in regex, so this can throw at runtime (invalid regex) and break prebuild. Prefer plain string replacement (e.g. split/join or replaceAll) or escape the key before constructing the RegExp. Also consider resolving the template directory in an ESM-safe way (avoid relying on __dirname in the lib/module build).
| public func application( | ||
| _ application: UIApplication, | ||
| didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil | ||
| ) -> Bool { | ||
| ((expoDelegate?.application(application, didFinishLaunchingWithOptions: launchOptions)) != nil) | ||
| } |
There was a problem hiding this comment.
This method returns true whenever expoDelegate?.application(...) is non-nil, even if the delegate returns false. It should return the delegate's Bool result (or a sensible default) rather than checking for nil.
| 1. Add the following block to your `app.json` file: | ||
|
|
||
| ```json | ||
| { | ||
| "plugins": [ | ||
| "@callstack/react-native-brownfield", | ||
| ] | ||
| } | ||
| ``` |
There was a problem hiding this comment.
The app.json example is not valid Expo config JSON: the plugins array should be nested under the top-level "expo" key, and the snippet currently has an extra trailing comma/bracket. This will confuse users copy/pasting the example.
| - `build:example:android-consumer` - builds the example native Android consumer app (`apps/AndroidApp`) | ||
| - `build:example:ios-consumer` - builds the example native Apple consumer app (`apps/AppleApp`) No newline at end of file | ||
| - `build:example:android-consumer:expo` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the Expo RN app (`apps/ExpoApp`) artifact | ||
| - - `build:example:android-consumer:vanilla` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the vanilla RN app (`apps/RNApp`) artifact |
There was a problem hiding this comment.
This list item is accidentally double-bulleted (- - ...), which breaks markdown formatting. Remove the extra - so it renders as a single bullet.
| - - `build:example:android-consumer:vanilla` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the vanilla RN app (`apps/RNApp`) artifact | |
| - `build:example:android-consumer:vanilla` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the vanilla RN app (`apps/RNApp`) artifact |

Summary
Our brownfield plugin has previously worked with Expo, but required manual setup. This PR provides an Expo config plugin to automate that, and a demo Expo application for testing. The plugin applies all the setup steps we described in brownfield's docs, automatically, during expo prebuild. Additionally:
Demo apps & CI have been adjusted:
CLI has been adjusted to run the Expo prebuild if expo is both: detected installed & explicitly listed in the package.json so as not to produce false positives in monorepo setups.
Finally, the CLI is updated to work seamlessly with Expo projects.
Docs:
TODO
Verify the
embedExpoDependenciesblock that previously took care of embedding all expo modules into the AAREvaluate how to use
ReactNativeBrownfieldalready established APIs with ExpoReactNativeHostManagerclass in the XCFramework which takes care of everything for ExpoReactNativeHostManager.loadViewand present RNScreenshots
Android CLI screenshots
Apple app: Expo variant
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-02-18.at.14.46.23.mov
Apple app: vanilla variant
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-02-18.at.14.54.00.mov
Android app: Expo variant
Screen.Recording.2026-02-18.at.2.39.29.PM.mov
Android app: vanilla variant
Screen.Recording.2026-02-18.at.2.40.20.PM.mov
Test plan
CI green.